/* Copyright (c) 2006, 2009, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */
 
package com.cburch.logisim.circuit;

import java.awt.Graphics;
import java.util.AbstractList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import com.cburch.logisim.comp.Component;
import com.cburch.logisim.comp.ComponentFactory;
import com.cburch.logisim.comp.ComponentDrawContext;
import com.cburch.logisim.comp.ComponentListener;
import com.cburch.logisim.comp.EndData;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeListener;
import com.cburch.logisim.data.AttributeOption;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.data.Attributes;
import com.cburch.logisim.data.BitWidth;
import com.cburch.logisim.data.Bounds;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.tools.CustomHandles;
import com.cburch.logisim.collections.Cache;
import com.cburch.logisim.UItools.GraphicsUtil;
import com.cburch.logisim.util.IntegerFactory;
import com.cburch.logisim.util.Strings;

public final class Wire implements Component, AttributeSet, CustomHandles {
    /** Stroke width when drawing wires. */
    public static final int WIDTH = 3;

    public static final Object horz_value = "horizontal";
    public static final Object vert_value = "vertical";
    public static final Attribute dir_attr
        = Attributes.forOption("direction", Strings.getter("wireDirectionAttr", Strings.circuitSource),
            new AttributeOption[] {
                new AttributeOption("horizontal", Strings.getter("wireDirectionHorzOption", Strings.circuitSource)),
                new AttributeOption("vertical", Strings.getter("wireDirectionVertOption", Strings.circuitSource)),
            });
    public static final Attribute len_attr
        = Attributes.forInteger("length", Strings.getter("wireLengthAttr", Strings.circuitSource));

    private static final List ATTRIBUTES = Arrays.asList(new Attribute[] {
            dir_attr, len_attr
    });
    private static final Cache cache = new Cache();

    /**
	 * Creates a wire instance then gets the wire object out of the Cache.
	 *
	 * @param	Location	first end point of the wire
	 * @param	Location	second end point of the wire
	 * @return	Wire		new Wire component
	 */
    public static Wire create(Location e0, Location e1) {
        return (Wire) cache.get(new Wire(e0, e1));
    }
    
    private class EndList extends AbstractList {
        public Object get(int i) {
            return getEnd(i);
        }
        public int size() { return 2; }
    }

    final Location e0;
    final Location e1;
    final boolean is_x_equal;

    /**
	 * Private constructor. The assignment of locations inside the wire is 
	 * determined by the X coordinates of the two locations where the
	 * location with the smaller X coordinate is saved as e0. In the event
	 * of both locations having the same X coordinate, the Y coordinates
	 * of the location are used.
	 * 
	 * @param	Location	first end point of the wire
	 * @param	Location	second end point of the wire
	 */
    private Wire(Location e0, Location e1) {
        this.is_x_equal = e0.getX() == e1.getX();
        if(is_x_equal) {
            if(e0.getY() > e1.getY()) {
                this.e0 = e1;
                this.e1 = e0;
            } else {
                this.e0 = e0;
                this.e1 = e1;
            }
        } else {
            if(e0.getX() > e1.getX()) {
                this.e0 = e1;
                this.e1 = e0;
            } else {
                this.e0 = e0;
                this.e1 = e1;
            }
        }
    }
    
    /**
	 * Check if another object, casted as a wire, has the same locations
	 * (e0 and e1) as the wire.
	 * 
	 * @param	Object		the object the wire may be equal to
	 * @return	boolean		true if the object equals this wire
	 */
    public boolean equals(Object other) {
        if(!(other instanceof Wire)) return false;
        Wire w = (Wire) other;
        return w.e0.equals(this.e0) && w.e1.equals(this.e1);
    }
    
    /**
	 * Get the hash code for the Wire object.
	 * 
	 * @return	int		the hash code for the wire
	 */
    public int hashCode() {
        return e0.hashCode() * 31 + e1.hashCode();
    }

    /**
	 * Get the length of the wire. Since wires can only be horizontal or
	 * vertical lines this is rather simple and just returns the
	 * difference of the X and Y locations added together.
	 * 
	 * @return	the length of the wire
	 */
    public int getLength() {
        return (e1.getY() - e0.getY()) + (e1.getX() - e0.getX());
    }

    /**
	 * Get a string representation of a Wire object.
	 * 
	 * @return	String	the string representation of a Wire object
	 */
    public String toString() {
        return "Wire[" + e0 + "-" + e1 + "]";
    }

    //
    // Component methods
    //
    // (Wire never issues ComponentEvents, so we don't need to track listeners)
    /**
	 * Unused inherited method.
	 * 
	 * @return	void
	 */
    
    public void addComponentListener(ComponentListener e) { }
    
    /**
	 * Unused inherited method.
	 * 
	 * @return	void
	 */
    public void removeComponentListener(ComponentListener e) { }

    public Object getAttributeValue(Attribute attr)
    {
    	return this.getAttributeSet().getValue(attr);
    }
    
    public void setAttributeValue(Attribute attr, Object value)
    {
        this.getAttributeSet().setValue(attr, value);
    }
    
    /**
	 * Gets the instance of the static WireFactory associated with the program.
	 * 
	 * @return	ComponentFactory		the static WireFactory in the program.
	 */
    public ComponentFactory getFactory() {
        return WireFactory.instance;
    }

    /**
	 * Get the AttributeSet of this Wire object. Just returns the object at the
	 * moment.
	 * 
	 * @return	AttributeSet		the attribute set of this instance
	 */
    public AttributeSet getAttributeSet() {
        return this;
    }

    // location/extent methods
    /**
	 * Get the first location (e0) of this Wire object.
	 * 
	 * @return	Location
	 */
    public Location getLocation() {
        return e0;
    }

    /**
	 * Get the boundary box containing this Wire object. Odd dimensions, two units up 
	 * and left from the e0 point but 5 down and right from the e1 point. *REVISIT*
	 * 
	 * @return	Bounds		the bounding box of this Wire
	 */
    public Bounds getBounds() {
        int x0 = e0.getX();
        int y0 = e0.getY();
        return Bounds.create(x0 - 2, y0 - 2,
            e1.getX() - x0 + 5, e1.getY() - y0 + 5);
    }

    /**
	 * Get the boundary box containing this Wire object. This method is required from
	 * the interface it extends and just calls the previous getBounds() method. The
	 * Graphics object is not used.
	 * 
	 * @param	Graphics		the graphics object this Wire is associated with
	 * @return	Bounds		the bounding box of this Wire
	 */
    public Bounds getBounds(Graphics g) {
        return getBounds();
    }

    /**
	 * Check if the Wire contains a location, ie, if the location is within a
	 * bounding box (different from getBounds()) of the Wire. If the Wire is 
	 * vertical the width is 2 units wide and between the Y locations. If the 
	 * wire is horizontal the height is 2 units wide and between the X locations.
	 *  
	 * @param	Location		the location that may be on the Wire
	 * @return	boolean		true if the Wire contains the location
	 */
    public boolean contains(Location q) {
        int qx = q.getX();
        int qy = q.getY();
        if(is_x_equal) {
            int wx = e0.getX();
            return qx >= wx - 2 && qx <= wx + 2
                && e0.getY() <= qy && qy <= e1.getY();
        } else {
            int wy = e0.getY();
            return qy >= wy - 2 && qy <= wy + 2
                && e0.getX() <= qx && qx <= e1.getX();
        }
    }

    /**
	 * Check if the Wire contains a location, ie, if the location is within a
	 * bounding box (different from getBounds()) of the Wire. If the Wire is 
	 * vertical the width is 2 units wide and between the Y locations. If the 
	 * wire is horizontal the height is 2 units wide and between the X locations. 
	 * This method is required from the interface it extends and just calls the 
	 * previous contains() method. The Graphics object is not used.
	 * 
	 * @param	Graphics		the graphics object this Wire is associated with
	 * @return	boolean		the bounding box of this Wire
	 */
    public boolean contains(Location pt, Graphics g) {
        return contains(pt);
    }

    //
    // propagation methods
    //
    /**
	 * Get the ends of the Wire. At the moment is returns a new EndList object.
	 * 
	 * @return	List		EndList that will get endPoints based on an index. (Endpoints
	 * 						not contained in object, I believe)	*REVISIT*
	 */
    public java.util.List getEnds() {
        return new EndList();
    }

    /**
	 * Get the data for the desired end point. Creates an EndData object
	 * with the location of the desired end point, an UNKNOWN BitWidth and
	 * INPUT_OUTPUT as the type.
	 * 
	 * @param	int		 
	 * @return	EndData		
	 */
    public EndData getEnd(int index) {
        Location loc = getEndLocation(index);
        return new EndData(loc, BitWidth.UNKNOWN,
                EndData.INPUT_OUTPUT);
    }

    /**
	 * Check if either ends of the Wire at at a location.
	 * 
	 * @param	Location		possible end point of the Wire
	 * @return	boolean		true if Wire ends on either side at given location	
	 */
    public boolean endsAt(Location pt) {
        return e0.equals(pt) || e1.equals(pt);
    }

    /**
	 * Sets each point of the wire as "dirty" so propagation is done correctly.
	 * 
	 * @param	CircuitState
	 * @return	void		
	 */
    public void propagate(CircuitState state) {
        // Normally this is handled by CircuitWires, and so it won't get
        // called. The exception is when a wire is added or removed
        state.markPointAsDirty(e0);
        state.markPointAsDirty(e1);
    }

    //
    // user interface methods
    //
    /**
	 * 
	 * 
	 * @param	ComponentDrawContext	
	 * @return	void		
	 */
    public void expose(ComponentDrawContext context) {
        java.awt.Component dest = context.getDestination();
        int x0 = e0.getX();
        int y0 = e0.getY();
        dest.repaint(x0 - 5, y0 - 5,
                e1.getX() - x0 + 10, e1.getY() - y0 + 10);
    }

    /**
	 * Draw the wire with the given context.
	 * 
	 * @param	ComponentDrawContext		context with which to draw the Wire	
	 * @return	void		
	 */
    public void draw(ComponentDrawContext context) {
        CircuitState state = context.getCircuitState();
        Graphics g = context.getGraphics();

        GraphicsUtil.switchToWidth(g, WIDTH);
        g.setColor(state.getValue(e0).getColor());
        g.drawLine(e0.getX(), e0.getY(),
            e1.getX(), e1.getY());
    }
    
    /**
	 * Get a feature of this Wire. At the moment this method only handles
	 * CustomHandles features.
	 * 
	 * @param	Object		key representing the type of feature requested
	 * @return	Object		this object if the key is a CustomHandle object
	 */
    public Object getFeature(Object key) {
        if(key == CustomHandles.class) return this;
        return null;
    }

    
    //
    // AttributeSet methods
    //
    // It makes some sense for a wire to be its own attribute, since
    // after all it is immutable.
    //
    public Object clone() { return this; }
    public void addAttributeListener(AttributeListener l) { }
    public void removeAttributeListener(AttributeListener l) { }
    public List getAttributes() { return ATTRIBUTES; }
    public boolean containsAttribute(Attribute attr) { return ATTRIBUTES.contains(attr); }
    public Attribute getAttribute(String name) {
        for(Iterator it = ATTRIBUTES.iterator(); it.hasNext(); ) {
            Attribute attr = (Attribute) it.next();
            if(name.equals(attr.getName())) return attr;
        }
        return null;
    }
    
    /**
	 * Get if the Wire is read only. Only returns true.
	 * 
	 * @param	Attribute		not used		
	 * @return	boolean		always returns true
	 */
    public boolean isReadOnly(Attribute attr) { return true; }
    
    /**
	 * Set if the Wire is read only. Is not currently supported.
	 * 
	 * @param	Attribute		not used		
	 * @param	boolean		true if the Wire is read only
	 * @return	void		
	 */
    public void setReadOnly(Attribute attr, boolean value) {
        throw new UnsupportedOperationException();
    }
    
    /**
	 * Get the value of one of the attributes of a Wire. Currently only 
	 * returns the direction (horizontal or vertical) or length of the 
	 * Wire.
	 * 
	 * @param	Attribute		the desired attribute to get the value of
	 * @return	Location		an object containing the value of the desired attribute,
	 * 							null if attribute is not found
	 */
    public Object getValue(Attribute attr) {
        if(attr == dir_attr) {
            return is_x_equal ? vert_value : horz_value;
        } else if(attr == len_attr) {
            return IntegerFactory.create(getLength());
        } else {
            return null;
        }
    }

    /**
	 * Set the value of an attribute. Since Wire is read only, this always
	 * throws an exception.
	 * 
	 * @param	Attribute		attribute to change the value of
	 * @param	Object		value to set the attribute to
	 * @return	void
	 */
    public void setValue(Attribute attr, Object value) {
        throw new IllegalArgumentException("read only attribute");
    }

    //
    // other methods
    //
    /**
	 * Get if the line is vertical or not.
	 * 
	 * @return	boolean		true if Wire is vertical
	 */
    public boolean isVertical() { return is_x_equal; }

    /**
	 * Get either the first (e0) or second (e1) point in the Wire.
	 * 
	 * @param	int		index of the point desired (0 for e0, otherwise e1)
	 * @return	Location		the desired point (0 for e0, otherwise e1)
	 */
    public Location getEndLocation(int index) { return index == 0 ? e0 : e1; }

    /**
	 * Get the first end of the Wire.
	 * 
	 * @return	Location		first end of the wire (e0)
	 */
    public Location getEnd0() { return e0; }

    /**
	 * Get the second end of the Wire.
	 * 
	 * @return	Location		first end of the wire (e1)
	 */
    public Location getEnd1() { return e1; }

    /**
	 * Get the opposite end of the Wire than the one given.
	 * 
	 * @param	Location		the end to get the opposite of
	 * @return	Location		the opposite end than the one given
	 */
    public Location getOtherEnd(Location loc) {
        return (loc.equals(e0) ? e1 : e0);
    }
    
    /**
	 * Check if this Wire shares an end point with the given Wire.
	 * 
	 * @param	Wire		the end to get the opposite of
	 * @return	boolean		true if this Wire shares an end point with the
	 * 						given Wire
	 */
    public boolean sharesEnd(Wire other) {
        return this.e0.equals(other.e0) || this.e1.equals(other.e0)
            || this.e0.equals(other.e1) || this.e1.equals(other.e1);
    }

    /**
	 * Check if a Wire overlaps this Wire. Call the next overlaps() method
	 * with the given Wire's end points at the locations.
	 * 
	 * @param	Wire		the Wire that may overlap this Wire
	 * @return	boolean		true if the Wire overlaps this Wire
	 */
    public boolean overlaps(Wire other) {
        return overlaps(other.e0, other.e1);
    }

    /**
	 * Check if the line between two locations overlaps any part of this Wire. The
	 * line must also start at the first end point (e0) of the Wire.
	 * 
	 * @param	Location		first point of line segment
	 * @param	Location		second point of line segment
	 * @return	boolean
	 */
    private boolean overlaps(Location q0, Location q1) {
        if(is_x_equal) {
            int x0 = q0.getX();
            return x0 == q1.getX() && x0 == e0.getX()
                && e1.getY() >= q0.getY() && e0.getY() <= q1.getY();
        } else {
            int y0 = q0.getY();
            return y0 == q1.getY() && y0 == e0.getY()
                && e1.getX() >= q0.getX() && e0.getX() <= q1.getX();
        }
    }
    
    /**
	 * Draw the handles of the Wire, one for each end point. 
	 * 
	 * @param	ComponentDrawContext		context onto which to draw the handles	
	 * @return	void
	 */
    public void drawHandles(ComponentDrawContext context) {
        context.drawHandle(e0);
        context.drawHandle(e1);
    }
}
